Skip to content

🛡️ Fix: Path System Array Index Out of Bounds Vulnerability#193

Closed
sotoJ24 wants to merge 4 commits intoSunsetLabs-Game:mainfrom
sotoJ24:main
Closed

🛡️ Fix: Path System Array Index Out of Bounds Vulnerability#193
sotoJ24 wants to merge 4 commits intoSunsetLabs-Game:mainfrom
sotoJ24:main

Conversation

@sotoJ24
Copy link

@sotoJ24 sotoJ24 commented Aug 24, 2025

📋 Description

Fixes a critical security vulnerability in the path system that could lead to contract panics and DoS attacks. The get_path_step() and advance_enemy_position() functions were using panic!() and assert() for input validation, making the game vulnerable to crashes from malicious or malformed inputs.

Closes #176

🔧 Changes Made

Security Improvements

  • ✅ Replaced panic-based validation with graceful error handling using Result<T, PathError>
  • ✅ Added comprehensive bounds checking before array access operations
  • ✅ Implemented safe path ID validation without contract termination
  • ✅ Enhanced enemy position updates with proper edge case handling

Code Quality

  • ✅ Introduced custom error types for better error categorization
  • ✅ Added event emissions for debugging and monitoring
  • ✅ Comprehensive test coverage for all edge cases and attack vectors
  • ✅ Maintained backward compatibility with existing game logic

📁 Files Modified

  • contract/src/systems/game.cairo - Core path system implementation

🧪 Testing

Screenshot from 2025-08-25 11-41-45 Screenshot from 2025-08-25 11-10-44 Screenshot from 2025-08-25 11-15-55

✅ Checklist

  • Security vulnerability eliminated
  • Comprehensive test coverage added
  • Backward compatibility maintained
  • Error handling implemented
  • Documentation updated
  • Code style follows project standards
  • Performance impact assessed (minimal)

📈 Performance Impact

Minimal overhead - Error checking adds negligible gas costs while preventing catastrophic failures.

🔍 Review Focus Areas

  1. Security: Verify all panic conditions are eliminated
  2. Logic: Confirm game mechanics remain unchanged
  3. Testing: Review edge case coverage
  4. Integration: Check compatibility with existing systems

Summary by CodeRabbit

  • New Features

    • Path queries now return explicit outcomes (success with coords, invalid, out-of-bounds, completed).
    • Safer enemy progression: enemies advance only when a path is valid and not completed.
    • Predefined paths updated: straight bottom-row, L-shaped, zigzag.
  • Bug Fixes

    • Prevented crashes for invalid path or index requests by returning safe results instead of panicking.
  • Tests

    • Tests updated to assert option/result-based behavior and non-panicking flows.

@coderabbitai
Copy link

coderabbitai bot commented Aug 24, 2025

Walkthrough

Adds safe, non-panicking path access and progression APIs: introduces PathResult, converts path accessors to Option/PathResult returns, adds bounds-checked helpers (safe get_step_from_array, get_path_length, is_valid_path, is_valid_index), safe completion checks, and a non-panicking advance_enemy_position; tests updated accordingly.

Changes

Cohort / File(s) Summary
Path public types & API
contract/src/systems/game.cairo
Added pub enum PathResult { Success: (u32,u32), InvalidPath, IndexOutOfBounds, PathCompleted }; changed get_path_step to return Option<(u32,u32)>; added get_path_step_result.
Safe path utilities & validation
contract/src/systems/game.cairo
Replaced panicking access with get_step_from_array(...) -> Option<(u32,u32)>; added get_path_length, is_valid_path, is_valid_index; get_path_data uses Option<Span<(u32,u32)>>.
Path progression logic
contract/src/systems/game.cairo
Added advance_enemy_position(ref enemy: Enemy, path_id: u64, current_index: u32) -> Enemy; added is_path_completed_safe(path_id, index) -> Option<bool>; callers updated to use Option/PathResult flow.
Predefined paths & tests
contract/src/systems/game.cairo, contract/src/tests/test_path_system.cairo
Defined three predefined paths (IDs 0,1,2) and updated tests to assert Some((x,y)) / None; removed panic-based expectations and adjusted is_path_completed tests.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Caller as Game Logic
  participant PS as PathSystem
  participant Data as Path Data

  Caller->>PS: advance_enemy_position(enemy, path_id, index)
  PS->>PS: is_valid_path(path_id)?
  alt Invalid path
    PS-->>Caller: return enemy unchanged
  else Valid path
    PS->>PS: is_valid_index(path_id,index)?
    alt Index invalid or completed
      PS-->>Caller: return enemy unchanged
    else Valid and not completed
      PS->>Data: get_path_data(path_id)
      Data-->>PS: Some(Span)
      PS->>PS: get_step_from_array(span, index+1)
      PS-->>Caller: Updated enemy (coords, index+1)
    end
  end
Loading
sequenceDiagram
  autonumber
  actor Caller as External
  participant PS as PathSystem

  Caller->>PS: get_path_step_result(path_id, index)
  PS->>PS: validate path_id
  alt Invalid path_id
    PS-->>Caller: PathResult::InvalidPath
  else validate index
    alt Index out of bounds
      PS-->>Caller: PathResult::IndexOutOfBounds
    else path already completed
      PS-->>Caller: PathResult::PathCompleted
    else OK
      PS-->>Caller: PathResult::Success(x,y)
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • KevinMB0220

Pre-merge checks (5 passed)

✅ Passed checks (5 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly summarizes the main change of fixing the path system's array index out-of-bounds vulnerability and clearly reflects the pull request's objective.
Linked Issues Check ✅ Passed All acceptance criteria from issue #176 are met: panic assertions were replaced with Option- and PathResult-based error handling, bounds checks and path ID validations are in place, enemy advancement logic handles invalid inputs, and tests cover both valid and invalid scenarios.
Out of Scope Changes Check ✅ Passed The changes strictly focus on enhancing path system safety and related tests, with no modifications outside the scope of array bounds validation and error handling in game.cairo.
Description Check ✅ Passed The description directly addresses the security issue by explaining the switch from panic-based validation to graceful error handling and outlines the key changes related to bounds checking, safe path validation, and test updates.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Poem

"I hop where checks keep edges neat,
No panic trips my little feet.
With Option crumbs and Result spry,
I follow paths beneath the sky.
A careful rabbit — safe and sweet 🥕"

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
		  - name: "Undocumented Breaking Changes"
			  mode: "warning"
			  instructions: |
				  Flag potential breaking changes that are not documented:
				  1. Identify changes to public APIs/exports, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints (including removed/renamed items and changes to types, required params, return values, defaults, or behavior).
				  2. Ignore purely internal/private changes (e.g., code not exported from package entry points or marked internal).
				  3. Verify documentation exists: a "Breaking Change" section in the PR description and updates to CHANGELOG.md.

Please share your feedback with us on this Discord post.

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sotoJ24 sotoJ24 changed the title Feat: Path System Array Index Out of Bounds Vulnerability 🛡️ Fix: Path System Array Index Out of Bounds Vulnerability Aug 25, 2025
@sotoJ24 sotoJ24 marked this pull request as ready for review August 25, 2025 17:47
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
contract/src/systems/game.cairo (1)

303-349: Fix unsafe Span return from locally-allocated Array

The current implementation of get_path_data returns an Option<Span<(u32, u32)>> where each Span points into an Array<T> that was allocated inside the callee. In Cairo v1, Span is a non-owning view and the backing Array is dropped when the function returns—making the returned Span a dangling reference. This must be corrected to avoid undefined behavior.

• Location: contract/src/systems/game.cairo, lines 303–349
• Issue: Returning only Span<T> does not transfer ownership of the underlying Array<T> to the caller.
• Remedy: Return the owning Array<(u32, u32)> (or another owning container) so that its lifetime outlives the function, then the caller can derive a Span as needed.

Suggested fix (one of two approaches):

  1. Return the Array itself
    Change the signature to return Option<Array<(u32, u32)>> and update call sites to derive a span:

    - fn get_path_data(path_id: u64) -> Option<Span<(u32, u32)>> {
    + fn get_path_data(path_id: u64) -> Option<Array<(u32, u32)>> {
          match path_id {
              0 => {
                  let path_0_steps = array![
                      (0_u32, 5_u32),
                      (1_u32, 5_u32),
                      (2_u32, 5_u32),
                      (3_u32, 5_u32),
                      (4_u32, 5_u32),
                      (5_u32, 5_u32)
                  ];
    -             Option::Some(path_0_steps.span())
    +             Option::Some(path_0_steps)
              },
              1 => {
                  let path_1_steps = array![
                      /* … */
                  ];
    -             Option::Some(path_1_steps.span())
    +             Option::Some(path_1_steps)
              },
              2 => {
                  let path_2_steps = array![
                      /* … */
                  ];
    -             Option::Some(path_2_steps.span())
    +             Option::Some(path_2_steps)
              },
              _ => Option::None,
          }
    }

    Then callers can do:

    let steps_arr = get_path_data(path_id)?;
    let steps_span = steps_arr.span();
    let point = steps_span[idx];
  2. Move static path data into module-level storage
    If the set of paths is fixed, define them once as global constants or in a lazily-initialized static, avoiding per-call allocation:

    const PATH_0: Array<(u32, u32)> = array![(0,5), (1,5), …];
    /* similarly for PATH_1, PATH_2 */
    
    fn get_path_data(path_id: u64) -> Option<Span<(u32, u32)>> {
        match path_id {
            0 => Some(PATH_0.span()),
            1 => Some(PATH_1.span()),
            2 => Some(PATH_2.span()),
            _ => None,
        }
    }

Either approach ensures the backing buffer outlives the function call, eliminating dangling spans.

🧹 Nitpick comments (7)
contract/src/systems/game.cairo (7)

212-220: PathCompleted is never produced; wire it into callers or remove it

You introduced PathResult::PathCompleted but no code path currently returns it. Prefer to return PathCompleted from get_path_step_result when index == path_length to distinguish "end-of-path" from generic OOB, or drop the variant to avoid dead code.

Would you like me to update get_path_step_result and add tests that assert PathCompleted is returned at exactly index == length?


224-231: get_path_step: LGTM; consider early length check to avoid building arrays

The implementation is correct and panic-free. Minor micro-optimization: check length first to skip array materialization when the index is obviously invalid.

Apply this diff if you want the early-exit:

-        fn get_path_step(path_id: u64, index: u32) -> Option<(u32, u32)> {
-            // Validate path_id first
-            let path_data = Self::get_path_data(path_id);
-            match path_data {
-                Option::None => Option::None,
-                Option::Some(steps) => Self::get_step_from_array(steps, index),
-            }
-        }
+        fn get_path_step(path_id: u64, index: u32) -> Option<(u32, u32)> {
+            match Self::get_path_length(path_id) {
+                Option::None => Option::None,
+                Option::Some(len) => {
+                    if index >= len {
+                        Option::None
+                    } else {
+                        match Self::get_path_data(path_id) {
+                            Option::None => Option::None,
+                            Option::Some(steps) => Self::get_step_from_array(steps, index),
+                        }
+                    }
+                }
+            }
+        }

233-247: Return PathCompleted at index == length and reserve IndexOutOfBounds for index > length

This makes PathResult exhaustive and semantically precise, and avoids constructing steps when the index is already known to be terminal or invalid.

Apply this diff:

-        fn get_path_step_result(path_id: u64, index: u32) -> PathResult {
-            // Validate path_id first
-            let path_data = Self::get_path_data(path_id);
-            match path_data {
-                Option::None => PathResult::InvalidPath,
-                Option::Some(steps) => {
-                    match Self::get_step_from_array(steps, index) {
-                        Option::None => PathResult::IndexOutOfBounds,
-                        Option::Some(coords) => PathResult::Success(coords),
-                    }
-                }
-            }
-        }
+        fn get_path_step_result(path_id: u64, index: u32) -> PathResult {
+            match Self::get_path_length(path_id) {
+                Option::None => PathResult::InvalidPath,
+                Option::Some(len) => {
+                    if index == len {
+                        PathResult::PathCompleted
+                    } else if index > len {
+                        PathResult::IndexOutOfBounds
+                    } else {
+                        match Self::get_path_data(path_id) {
+                            Option::None => PathResult::InvalidPath, // defensive, should not happen
+                            Option::Some(steps) => match Self::get_step_from_array(steps, index) {
+                                Option::None => PathResult::IndexOutOfBounds, // defensive
+                                Option::Some(coords) => PathResult::Success(coords),
+                            },
+                        }
+                    }
+                }
+            }
+        }

248-276: Behavior is safe and non-panicking; consider emitting lightweight events or returning the new index

The off-by-one logic using next_index and the early completion check looks correct and prevents OOB. If observability is desired (PR description mentions events), consider emitting an event on success/failure or returning (Enemy, Option) to propagate the next index to the caller without extra storage reads.

Additional code (outside this hunk) to define minimal events:

// Add near other events in this module
#[event]
pub enum PathEvent {
    Advanced: (enemy_id: u64, index: u32, x: u32, y: u32),
    AdvanceIgnored: (enemy_id: u64, reason: felt252, index: u32),
}

And then inside this function (illustrative only; adjust fields as available):

// on success
emit PathEvent::Advanced(enemy_id: enemy.id, index: next_index, x: next_x, y: next_y);
// on invalid path/index
emit PathEvent::AdvanceIgnored(enemy_id: enemy.id, reason: 'invalid_path_or_index', index: next_index);

PR description states “Added event emissions,” but this file doesn’t emit any. Are events introduced elsewhere in this PR? If not, should we include the above minimal events here?


278-294: Avoid duplication: reuse get_path_length

Path lengths are hardcoded here and in get_path_length. Collapse to a single source of truth.

Apply this diff:

-        fn is_path_completed_safe(path_id: u64, index: u32) -> Option<bool> {
-            // Get the path length for each predefined path
-            let path_length = match path_id {
-                0 => Option::Some(6_u32), // Path 0 has 6 steps (indices 0-5)
-                1 => Option::Some(7_u32), // Path 1 has 7 steps (indices 0-6)  
-                2 => Option::Some(9_u32), // Path 2 has 9 steps (indices 0-8)
-                _ => Option::None,
-            };
-
-            match path_length {
-                Option::None => Option::None,
-                Option::Some(length) => Option::Some(index >= length),
-            }
-        }
+        fn is_path_completed_safe(path_id: u64, index: u32) -> Option<bool> {
+            match Self::get_path_length(path_id) {
+                Option::None => Option::None,
+                Option::Some(length) => Option::Some(index >= length),
+            }
+        }

360-368: get_path_length: LGTM; single source of truth used elsewhere

Values match the arrays (6, 7, 9). Once you update is_path_completed_safe to reuse this, duplication is gone.


370-376: is_valid_path: LGTM

Straightforward and explicit. If path count grows, consider switching to Self::get_path_length(path_id).is_some() to avoid updating in two places.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2a116e9 and 4cd38c7.

📒 Files selected for processing (1)
  • contract/src/systems/game.cairo (5 hunks)
🔇 Additional comments (2)
contract/src/systems/game.cairo (2)

351-358: get_step_from_array: LGTM

Bounds-checked read avoids any out-of-bounds panics while using Span::at safely.


378-384: is_valid_index: LGTM

Properly leverages get_path_length; safe and non-panicking.

Comment on lines +212 to +247
// Result type for path operations
#[derive(Copy, Drop, Serde)]
pub enum PathResult {
Success: (u32, u32),
InvalidPath,
IndexOutOfBounds,
PathCompleted,
}

#[generate_trait]
pub impl PathSystemImpl of PathSystemTrait {
/// Returns the (x, y) coordinates for the given path_id and step index
fn get_path_step(path_id: u64, index: u32) -> (u32, u32) {
// Predefined paths
fn get_path_step(path_id: u64, index: u32) -> Option<(u32, u32)> {
// Validate path_id first
let path_data = Self::get_path_data(path_id);
match path_data {
Option::None => Option::None,
Option::Some(steps) => Self::get_step_from_array(steps, index),
}
}

/// Returns the (x, y) coordinates for the given path_id and step index with detailed result
fn get_path_step_result(path_id: u64, index: u32) -> PathResult {
// Validate path_id first
let path_data = Self::get_path_data(path_id);
match path_data {
Option::None => PathResult::InvalidPath,
Option::Some(steps) => {
match Self::get_step_from_array(steps, index) {
Option::None => PathResult::IndexOutOfBounds,
Option::Some(coords) => PathResult::Success(coords),
}
}
}
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Add/verify tests for edge cases and legacy API behavior

Given the high-severity bug being addressed, please ensure tests cover:

  • Invalid path_id for all public helpers (expect None/InvalidPath, no panics).
  • Index exactly at length (expect PathCompleted from get_path_step_result, no movement in advance_enemy_position).
  • Index > length (expect None/IndexOutOfBounds, no movement).
  • Last valid step followed by call to advance (enemy stays put; path considered completed).
  • Legacy is_path_completed no longer panics (after the suggested change), and callers are updated accordingly.

I can draft these tests if helpful.

Run this script to find potential call-site breakages due to get_path_step signature change:

Also applies to: 248-276, 278-294, 295-301, 351-358


🏁 Script executed:

#!/bin/bash
# Look for calls expecting a tuple instead of Option
rg -nP -C2 '\bget_path_step\s*\('

# Ensure no panics remain in the path system APIs
rg -nP -C2 '\bpanic!\(|\bassert\s*\(' contract/src/systems/game.cairo

Length of output: 4227


🏁 Script executed:

#!/bin/bash
# Search for get_path_step_result in tests
rg -nP -C2 'get_path_step_result' contract/src/tests || echo "No matches for get_path_step_result"

# Search for is_path_completed in tests
rg -nP -C2 'is_path_completed' contract/src/tests || echo "No matches for is_path_completed"

Length of output: 3349


Critical fixes required: update tests and align legacy API behavior

  • Tests in contract/src/tests/test_path_system.cairo must be updated to match the new get_path_step signature and behavior:
    • test_get_path_step_valid_paths
      • Change from let (x, y) = PathSystemImpl::get_path_step(...) to
        let coords = PathSystemImpl::get_path_step(...);
        assert!(coords == Option::Some((expected_x, expected_y)), "");
    • test_get_path_step_invalid_path and test_get_path_step_index_out_of_bounds
      • Remove #[should_panic] and change to
        assert_eq!(PathSystemImpl::get_path_step(invalid_path, idx), Option::None);
  • Add new tests for get_path_step_result covering:
    • InvalidPath when path_id not found
    • IndexOutOfBounds when index > last step
    • PathCompleted when index == length
    • Success((x, y)) for valid steps
  • In contract/src/systems/game.cairo, the old is_path_completed still panics on invalid path_id. Per the original suggestion, it should return a safe result (e.g., false or Option<bool>) and callers updated; either remove the panic! or wrap it in a safe variant, then adjust or remove the #[should_panic] test accordingly.

I can draft the updated tests and refactoring if that would help.

🤖 Prompt for AI Agents
In contract/src/systems/game.cairo around lines 212 to 247 the new get_path_step
signature returns Option<(u32,u32)> and get_path_step_result returns PathResult,
but tests and the legacy is_path_completed behavior weren't updated; update
tests in contract/src/tests/test_path_system.cairo to: replace destructuring let
(x,y) = PathSystemImpl::get_path_step(...) with let coords =
PathSystemImpl::get_path_step(...); assert that coords ==
Option::Some((expected_x, expected_y)) for valid cases, remove #[should_panic]
on invalid-path and out-of-bounds tests and
assert_eq!(PathSystemImpl::get_path_step(invalid_path, idx), Option::None), add
new tests for get_path_step_result asserting InvalidPath, IndexOutOfBounds,
PathCompleted (when index == length) and Success((x,y)) for valid indices; also
change is_path_completed implementation to not panic on missing path (return a
safe value or Option<bool>) and update any callers/tests to use the safe variant
or adjust assertions accordingly.

Comment on lines 295 to 301
/// Legacy function for backward compatibility - will panic if path completed
fn is_path_completed(path_id: u64, index: u32) -> bool {
match Self::is_path_completed_safe(path_id, index) {
Option::None => panic!("Invalid path_id"),
Option::Some(result) => result,
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Legacy is_path_completed still panics; replace with graceful behavior

Keeping a panicking branch reintroduces the class of failure we’re trying to eliminate. Prefer a safe default instead of panic, or deprecate this API.

Apply this diff:

-        /// Legacy function for backward compatibility - will panic if path completed
+        /// Legacy function for backward compatibility - now graceful on invalid path_id
         fn is_path_completed(path_id: u64, index: u32) -> bool {
-            match Self::is_path_completed_safe(path_id, index) {
-                Option::None => panic!("Invalid path_id"),
-                Option::Some(result) => result,
-            }
+            match Self::is_path_completed_safe(path_id, index) {
+                Option::Some(result) => result,
+                // Safer default: treat invalid paths as completed to prevent further advancement.
+                Option::None => true,
+            }
         }

Please run the verification script below to ensure no external callers rely on the panic behavior:


🏁 Script executed:

#!/bin/bash
# Find all call sites of is_path_completed across the repo
rg -nP -C2 '\bis_path_completed\s*\('

Length of output: 3425


Legacy is_path_completed behavior change requires test updates

The proposed change to return a safe default (true) instead of panicking on invalid path_id will break existing tests that expect a panic. You’ll need to update the test suite alongside this API change:

• In contract/src/systems/game.cairo (lines ~295–301), you’ve replaced

Option::None => panic!("Invalid path_id"),

with

Option::None => true,

– this alters the behavior on invalid inputs.

• In contract/src/tests/test_path_system.cairo (lines 91–95), the #[should_panic] test for an invalid path_id must be revised:
– Remove the #[should_panic] attribute.
– Change the test to assert that the function returns true for an invalid path. For example:

- #[should_panic]
- fn test_is_path_completed_invalid_path() {
-     let _ = PathSystemImpl::is_path_completed(99_u64, 0_u32);
- }
+ #[test]
+ fn test_is_path_completed_invalid_path_returns_true() {
+     assert!(
+         PathSystemImpl::is_path_completed(99_u64, 0_u32),
+         "Invalid path IDs should be treated as completed"
+     );
+ }

Please apply these updates to ensure test coverage aligns with the new, non-panic behavior.

🤖 Prompt for AI Agents
In contract/src/systems/game.cairo around lines 295–301 you changed Option::None
handling from panic!("Invalid path_id") to returning true, which changes
behavior for invalid path_id inputs; update the tests in
contract/src/tests/test_path_system.cairo (around lines 91–95) by removing the
#[should_panic] attribute and replacing the expectation with an assertion that
is_path_completed(invalid_path_id, index) returns true, and scan the test suite
for any other tests expecting a panic on invalid path_id and convert them to
assert the new true default instead.

@KevinMB0220
Copy link
Contributor

@sotoJ24 pleas fix the tests

@sotoJ24
Copy link
Author

sotoJ24 commented Aug 29, 2025

@sotoJ24 pleas fix the tests

On it

@sotoJ24
Copy link
Author

sotoJ24 commented Sep 2, 2025

@sotoJ24 pleas fix the tests

On it

Submitting changes soon

@sotoJ24
Copy link
Author

sotoJ24 commented Sep 3, 2025

Done

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
contract/src/tests/test_path_system.cairo (1)

144-154: Remove panic expectation; advance on dead enemy should be a no-op

To fully eliminate panic paths, advancing a dead enemy should not panic and should leave the enemy unchanged. Update the test accordingly (and see paired code change in advance_enemy_position).

Apply this diff:

-    #[test]
-    #[should_panic]
-    fn test_advance_dead_enemy_panics() {
+    #[test]
+    fn test_advance_dead_enemy_noop() {
         let mut enemy = enemy_ok();
 
         // Kill the enemy
         enemy = EnemySystem::take_damage(@enemy, 100_u32);
         assert!(!enemy.is_alive, "Enemy should be dead");
 
-        // Try to advance dead enemy
-        let _ = PathSystemImpl::advance_enemy_position(ref enemy, 0_u64, 0_u32);
+        // Advancing a dead enemy should be a no-op (no panic, no movement)
+        let result = PathSystemImpl::advance_enemy_position(ref enemy, 0_u64, 0_u32);
+        assert!(result.x == enemy.x && result.y == enemy.y && !result.is_alive, "Dead enemy should not move");
     }

I can also add tests for get_path_step_result covering Success, PathCompleted, IndexOutOfBounds, and InvalidPath.

🧹 Nitpick comments (5)
contract/src/systems/game.cairo (5)

212-219: PathResult::PathCompleted is defined but never returned

Currently unused by get_path_step_result; consider distinguishing “index == length” from “index > length”.


278-293: Avoid duplicating path lengths; derive from path data

Single source of truth prevents drift if paths change.

Apply this diff:

-        fn is_path_completed_safe(path_id: u64, index: u32) -> Option<bool> {
-            // Get the path length for each predefined path
-            let path_length = match path_id {
-                0 => Option::Some(6_u32), // Path 0 has 6 steps (indices 0-5)
-                1 => Option::Some(7_u32), // Path 1 has 7 steps (indices 0-6)  
-                2 => Option::Some(9_u32), // Path 2 has 9 steps (indices 0-8)
-                _ => Option::None,
-            };
-
-            match path_length {
-                Option::None => Option::None,
-                Option::Some(length) => Option::Some(index >= length),
-            }
-        }
+        fn is_path_completed_safe(path_id: u64, index: u32) -> Option<bool> {
+            match Self::get_path_data(path_id) {
+                Option::None => Option::None,
+                Option::Some(steps) => {
+                    let length: u32 = steps.len().try_into().unwrap();
+                    Option::Some(index >= length)
+                }
+            }
+        }

295-301: Update outdated doc comment for is_path_completed

The function no longer panics on invalid path_id; reflect the new behavior.

Apply this diff:

-        /// Legacy function for backward compatibility - will panic if path completed
+        /// Legacy function for backward compatibility - graceful on invalid path_id (returns true)
         fn is_path_completed(path_id: u64, index: u32) -> bool {

360-368: Derive path length from data instead of hardcoding

Removes duplication and keeps length consistent with get_path_data.

Apply this diff:

-        fn get_path_length(path_id: u64) -> Option<u32> {
-            match path_id {
-                0 => Option::Some(6_u32),
-                1 => Option::Some(7_u32),
-                2 => Option::Some(9_u32),
-                _ => Option::None,
-            }
-        }
+        fn get_path_length(path_id: u64) -> Option<u32> {
+            match Self::get_path_data(path_id) {
+                Option::Some(steps) => Option::Some(steps.len().try_into().unwrap()),
+                Option::None => Option::None,
+            }
+        }

370-376: Use get_path_data to validate path IDs

Avoids maintaining a separate list.

Apply this diff:

-        fn is_valid_path(path_id: u64) -> bool {
-            match path_id {
-                0 | 1 | 2 => true,
-                _ => false,
-            }
-        }
+        fn is_valid_path(path_id: u64) -> bool {
+            match Self::get_path_data(path_id) {
+                Option::Some(_) => true,
+                Option::None => false,
+            }
+        }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4cd38c7 and 51bebeb.

📒 Files selected for processing (2)
  • contract/src/systems/game.cairo (5 hunks)
  • contract/src/tests/test_path_system.cairo (2 hunks)
🔇 Additional comments (8)
contract/src/tests/test_path_system.cairo (3)

18-35: LGTM: valid path step assertions align with path data

The expected coordinates match the predefined paths.


85-90: LGTM: legacy is_path_completed behavior updated in tests

Treating invalid path IDs as completed avoids panics downstream.


160-175: LGTM: zigzag path traversal assertions match path 2

Checks are consistent with the defined zigzag sequence.

contract/src/systems/game.cairo (5)

224-231: LGTM: get_path_step safely validates path and index

Option-based flow removes the OOB panic surface.


317-317: LGTM: path data exposed via Option

Constructors and spans look correct.

Also applies to: 330-330, 345-345


351-358: LGTM: safe array access with bounds check

Prevents OOB before steps.at(...).


378-384: LGTM: index validation tied to computed length

Simple and correct.


212-301: All panic!/assert calls removed from the path subsystem and PathCompleted is correctly referenced in get_path_step_result.

Comment on lines +39 to 40
assert_eq!(PathSystemImpl::get_path_step(invalid_path, idx), Option::None);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix undefined variables in test (invalid_path, idx)

This test won’t compile; use concrete literals or define the variables.

Apply this diff:

-        assert_eq!(PathSystemImpl::get_path_step(invalid_path, idx), Option::None);
+        assert_eq!(PathSystemImpl::get_path_step(99_u64, 0_u32), Option::None);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert_eq!(PathSystemImpl::get_path_step(invalid_path, idx), Option::None);
}
assert_eq!(PathSystemImpl::get_path_step(99_u64, 0_u32), Option::None);
}
🤖 Prompt for AI Agents
In contract/src/tests/test_path_system.cairo around lines 39 to 40, the test
references undefined variables invalid_path and idx which causes a compile
error; replace them with concrete literals or add local let bindings before use
(e.g., define invalid_path with an appropriate Path value and idx with a
usize/u128 literal) and then call PathSystemImpl::get_path_step(invalid_path,
idx) asserting Option::None; ensure the types match the function signature and
the test imports any needed constructors or constants.

Comment on lines +44 to 45
assert_eq!(PathSystemImpl::get_path_step(invalid_path, idx), Option::None);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Correct the “index out of bounds” test to use a valid path with an OOB index

Currently reuses the invalid path variables; assert OOB on a valid path.

Apply this diff:

-        assert_eq!(PathSystemImpl::get_path_step(invalid_path, idx), Option::None);
+        // Path 0 has length 6 (valid indices 0..=5); 6 is out of bounds
+        assert_eq!(PathSystemImpl::get_path_step(0_u64, 6_u32), Option::None);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert_eq!(PathSystemImpl::get_path_step(invalid_path, idx), Option::None);
}
// Path 0 has length 6 (valid indices 0..=5); 6 is out of bounds
assert_eq!(PathSystemImpl::get_path_step(0_u64, 6_u32), Option::None);
🤖 Prompt for AI Agents
In contract/src/tests/test_path_system.cairo around lines 44 to 45, the test for
"index out of bounds" incorrectly reuses the invalid_path variable; update the
assertion to call PathSystemImpl::get_path_step with a valid path (e.g.,
valid_path) and an index that is out of bounds for that valid path (e.g., its
length or greater) so the test asserts Option::None on a valid path with an OOB
index.

@KevinMB0220
Copy link
Contributor

@sotoJ24 you still having a problems with the tests

@sotoJ24
Copy link
Author

sotoJ24 commented Sep 5, 2025

@sotoJ24 you still having a problems with the tests

On it

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
contract/src/systems/game.cairo (1)

246-259: Differentiate PathCompleted from IndexOutOfBounds in get_path_step_result.

Currently, index == length collapses into IndexOutOfBounds. Return PathCompleted for semantic precision and simpler callers.

-        fn get_path_step_result(path_id: u64, index: u32) -> PathResult {
-            // Validate path_id first
-            let path_data = Self::get_path_data(path_id);
-            match path_data {
-                Option::None => PathResult::InvalidPath,
-                Option::Some(steps) => {
-                    match Self::get_step_from_array(steps, index) {
-                        Option::None => PathResult::IndexOutOfBounds,
-                        Option::Some(coords) => PathResult::Success(coords),
-                    }
-                }
-            }
-        }
+        fn get_path_step_result(path_id: u64, index: u32) -> PathResult {
+            match Self::get_path_data(path_id) {
+                Option::None => PathResult::InvalidPath,
+                Option::Some(steps) => {
+                    let len: usize = steps.len();
+                    let i: usize = index.into();
+                    if i < len {
+                        PathResult::Success(*steps.at(i))
+                    } else if i == len {
+                        PathResult::PathCompleted
+                    } else {
+                        PathResult::IndexOutOfBounds
+                    }
+                }
+            }
+        }
🧹 Nitpick comments (6)
contract/src/systems/game.cairo (6)

225-233: Good error taxonomy with PathResult.

Clear variants; matches stated objectives. Consider adding an event mapping later if you plan to emit diagnostics per result.


291-306: Doc clarity: define “completed” precisely.

You return Some(index >= length). Add a brief note: “Completed when index >= length; last valid step is length - 1.” Helps align with get_path_step_result semantics.


316-362: Avoid reallocating path arrays on every call.

These paths are static; repeated array construction increases gas. Consider a single source of truth (e.g., one helper returning a Span per id that reuses data) or memoizing in a const-like pattern if feasible in Cairo 1.


373-381: get_path_length: OK, mirrors data.

Keep this in sync with get_path_data; consider deriving from the data to avoid duplication.


383-389: is_valid_path duplicates length mapping.

Minor DRY win: implement via get_path_length(path_id).is_some().

-        fn is_valid_path(path_id: u64) -> bool {
-            match path_id {
-                0 | 1 | 2 => true,
-                _ => false,
-            }
-        }
+        fn is_valid_path(path_id: u64) -> bool {
+            matches!(Self::get_path_length(path_id), Option::Some(_))
+        }

308-314: Update is_path_completed doc to reflect new behavior
The comment still states “will panic,” but the implementation now returns true for invalid paths instead of panicking. Update the doc as shown below; no callers or tests rely on panics.

-/// Legacy function for backward compatibility - will panic if path completed
+/// Legacy function for backward compatibility - now graceful:
+/// returns true if the path is completed or the path_id is invalid (no panics).
 fn is_path_completed(path_id: u64, index: u32) -> bool {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 51bebeb and 9ada55e.

📒 Files selected for processing (1)
  • contract/src/systems/game.cairo (5 hunks)
🔇 Additional comments (3)
contract/src/systems/game.cairo (3)

237-244: get_path_step: OK.

Non-panicking Option API with prior path validation is exactly what we want.


391-397: is_valid_index: LGTM.

Leverages get_path_length; aligns with non-panicking contract.


225-306: Verify no panics/asserts remain in PathSystemImpl; add tests for PathCompleted and IndexOutOfBounds

  • Grep for panic! or assert( in contract/src/systems/game.cairo lines 225–371 and remove any occurrences within the PathSystemImpl block.
  • Add or update unit tests under contract/src/tests/… to explicitly cover both:
    • PathResult::PathCompleted (when index == path_length)
    • PathResult::IndexOutOfBounds (when index > path_length)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (4)
contract/src/systems/game.cairo (4)

261-279: Advance logic looks safe and non-panicking

Alive-guard added; length-based completion check avoids overflow; next_index computed only when safe. Matches earlier guidance.


298-304: Update docstring to reflect graceful behavior on invalid path_id

Comment still mentions panic. Align the documentation with current behavior (returns true on invalid path).

Apply:

-        /// Legacy function for backward compatibility - will panic if path completed
+        /// Legacy function for backward compatibility - graceful on invalid path_id (returns true)
         fn is_path_completed(path_id: u64, index: u32) -> bool {
             match Self::is_path_completed_safe(path_id, index) {
                 Option::None => true,
                 Option::Some(result) => result,
             }
         }

353-361: Bounds-safe indexing implemented correctly

Single usize conversion used for both check and access eliminates TOCTOU/mismatch risks.


246-259: Differentiate PathCompleted (index == length) vs IndexOutOfBounds (index > length)

Currently, both collapse to IndexOutOfBounds via get_step_from_array. Return PathCompleted when index == length for precise control flow and parity with is_path_completed_safe.

Apply:

-        fn get_path_step_result(path_id: u64, index: u32) -> PathResult {
-            // Validate path_id first
-            let path_data = Self::get_path_data(path_id);
-            match path_data {
-                Option::None => PathResult::InvalidPath,
-                Option::Some(steps) => {
-                    match Self::get_step_from_array(steps, index) {
-                        Option::None => PathResult::IndexOutOfBounds,
-                        Option::Some(coords) => PathResult::Success(coords),
-                    }
-                }
-            }
-        }
+        fn get_path_step_result(path_id: u64, index: u32) -> PathResult {
+            match Self::get_path_data(path_id) {
+                Option::None => PathResult::InvalidPath,
+                Option::Some(steps) => {
+                    let len_u: usize = steps.len();
+                    let len: u32 = len_u.try_into().unwrap();
+                    if index < len {
+                        PathResult::Success(*steps.at(index.into()))
+                    } else if index == len {
+                        PathResult::PathCompleted
+                    } else {
+                        PathResult::IndexOutOfBounds
+                    }
+                }
+            }
+        }
🧹 Nitpick comments (2)
contract/src/systems/game.cairo (2)

281-296: Avoid duplicated path lengths; reuse get_path_length()

Hard-coding lengths here risks drift from get_path_length. Delegate to it instead.

Apply:

-        fn is_path_completed_safe(path_id: u64, index: u32) -> Option<bool> {
-            // Get the path length for each predefined path
-            let path_length = match path_id {
-                0 => Option::Some(6_u32), // Path 0 has 6 steps (indices 0-5)
-                1 => Option::Some(7_u32), // Path 1 has 7 steps (indices 0-6)  
-                2 => Option::Some(9_u32), // Path 2 has 9 steps (indices 0-8)
-                _ => Option::None,
-            };
-
-            match path_length {
-                Option::None => Option::None,
-                Option::Some(length) => Option::Some(index >= length),
-            }
-        }
+        fn is_path_completed_safe(path_id: u64, index: u32) -> Option<bool> {
+            match Self::get_path_length(path_id) {
+                Option::None => Option::None,
+                Option::Some(length) => Option::Some(index >= length),
+            }
+        }

363-371: OK: Centralized path lengths; consider a test to guard against drift

Constants match the arrays above. Add a test asserting get_path_length(id) equals get_path_data(id).unwrap().len() for each id.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9ada55e and 78b896e.

📒 Files selected for processing (1)
  • contract/src/systems/game.cairo (5 hunks)
🔇 Additional comments (5)
contract/src/systems/game.cairo (5)

225-233: Good: Clear non-panicking result enum introduced

PathResult variants cover the needed outcomes and align with the non-panicking goal.


320-320: LGTM: Path data returned via Span for predefined paths

Returning spans is consistent and avoids copying.

Also applies to: 333-333, 348-348


373-379: LGTM: is_valid_path is simple and fast

No further action.


381-387: LGTM: is_valid_index delegates to get_path_length

This keeps checks consistent across APIs.


237-244: No tuple destructuring of get_path_step found; API shift verified
All call sites now handle the Option return.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Path System Array Index Out of Bounds Vulnerability

2 participants

Comments